//
//  RenSurface.cpp
//  2006 John Wiggins
//
// This program is free software; you can redistribute it and/or modify  
// it under the terms of the GNU General Public License as published by  
// the Free Software Foundation; either version 2 of the License, or     
// (at your option) any later version.                                   
// 
// This program is distributed in the hope that it will be useful,       
// but WITHOUT ANY WARRANTY; without even the implied warranty of        
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         
// GNU General Public License for more details.                          
//     
//     You should have received a copy of the GNU General Public License     
//     along with this program; if not, write to the                         
//         Free Software Foundation, Inc.,                                       
//         59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.  
//-------------------------------------------------------------
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>

#define GLH_EXT_SINGLE_FILE
#include "glh/glh_extensions.h"
#include <GL/glu.h>

#include "Render/RenSurface.h"
//-------------------------------------------------------------
#define GET_GLERROR(err)                                          \
{                                                                 \
    err = glGetError();                                           \
    if (err != GL_NO_ERROR) {                                     \
    fprintf(stderr, "[%s line %d] GL Error: %s\n",                \
    __FILE__, __LINE__, gluErrorString(err));                     \
    fflush(stderr);                                               \
    }                                                             \
}

#define BUFFER_OFFSET(i) ((char *)NULL + (i))
//-------------------------------------------------------------
static void
CheckFramebufferStatus()
{
    GLenum status;
    status = (GLenum) glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
    switch(status)
    {
        case GL_FRAMEBUFFER_COMPLETE_EXT:
            break;
        case GL_FRAMEBUFFER_UNSUPPORTED_EXT:
            printf("Unsupported framebuffer format\n");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
            printf("Framebuffer incomplete, missing attachment\n");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_DUPLICATE_ATTACHMENT_EXT:
            printf("Framebuffer incomplete, duplicate attachment\n");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
            printf("Framebuffer incomplete, attached images must have same dimensions\n");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
            printf("Framebuffer incomplete, attached images must have same format\n");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
            printf("Framebuffer incomplete, missing draw buffer\n");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
            printf("Framebuffer incomplete, missing read buffer\n");
            break;
        default:
            assert(0);
    }
}
//-------------------------------------------------------------
RenSurface::RenSurface( RenColorFormat fmt, unsigned int width, unsigned int height )
: mWidth    ( width )
, mHeight   ( height )
, mFormat   ( fmt )
{
    mData = (unsigned char *)malloc( getDataSize() );
    pthread_mutex_init( &mLock, NULL );
    mAttrModified = false;
    mIsGPUResident = false;
    mPBOAllocated = false;
    mPixelBuffer = 0;
    mFBOAllocated = false;
    mTexture = 0;
    mFrameBuffer = 0;

    if ( !glh_init_extensions("GL_ARB_fragment_program "
                            "GL_ARB_vertex_program "
                            "GL_NV_float_buffer "
                            "GL_EXT_framebuffer_object "
                            "GL_EXT_pixel_buffer_object ") )
    {
        printf("Unable to load the following extension(s): %s\n\nExiting...\n", 
                        glh_get_unsupported_extensions());
        exit(-1);
    }
}
//-------------------------------------------------------------
RenSurface::RenSurface( RenSurface const & copy )
: mWidth    ( copy.mWidth )
, mHeight   ( copy.mHeight )
, mFormat   ( copy.mFormat )
{
    // XXX: should a copy of the frame data be made?
    mData = (unsigned char *)malloc( getDataSize() );
    pthread_mutex_init( &mLock, NULL );
    mAttrModified = false;
    mIsGPUResident = false;
    mPBOAllocated = false;
    mPixelBuffer = 0;
    mFBOAllocated = false;
    mTexture = 0;
    mFrameBuffer = 0;
}
//-------------------------------------------------------------
RenSurface::~RenSurface()
{
    free( mData );
    
    while ( pthread_mutex_destroy(&mLock) == EBUSY )
    {   
        usleep( 1000 );
    }

    cleanupPBO();
    cleanupFBO();
}
//-------------------------------------------------------------
unsigned char *
RenSurface::getData()
{
    if ( mIsGPUResident )
    {   
        downloadFromGPU();
    }

    return mData;
}
//-------------------------------------------------------------
unsigned int
RenSurface::getDataSize() const
{
    return getDataSize( mFormat, mWidth, mHeight );
}
//-------------------------------------------------------------
unsigned int
RenSurface::getDataSize( RenColorFormat fmt, unsigned int width, unsigned int height ) const
{
    unsigned int ret = width*height;

    switch ( fmt )
    {
        case GE_FMT_GRAY:
            ret *= 1;
            break;
        case GE_FMT_YUV422:
            ret *= 2;
            break;
        case GE_FMT_RGB:
            ret *= 3;
            break;
        case GE_FMT_RGBA:
            ret *= 4;
            break;
        default:
            ret = 0;
            break;
    }

    return ret;
}
//-------------------------------------------------------------
void
RenSurface::modifyAttributes( RenColorFormat fmt, unsigned int width, unsigned int height )
{
    if ( mIsGPUResident )
    {
        printf( "ERROR: trying to modify texture attributes while data is"
                        " resident on the GPU\n" );
        assert( mIsGPUResident == false );
    }

    if ( fmt != mFormat || width != mWidth || height != mHeight )
    {
        // do we need to reallocate the memory buffer?
        if ( getDataSize() != getDataSize(fmt,width,height) )
        {      
            mData = (unsigned char *)realloc( mData, getDataSize( fmt,width,height ) );
        }

        // set the new values
        mWidth = width;
        mHeight = height;
        mFormat = fmt;

        // set the dirty attributes flag
        // GL state will be updated when needed
        mAttrModified = true;

        // if there is a pixel buffer allocated, we don't have to do anything
        // but if there is a frame buffer allocated...
//      if (mFBOAllocated)
//      {
//          GLenum err;
//          // bind the texture
//          glBindTexture(GL_TEXTURE_RECTANGLE_NV, mTexture);
//          // set it to the new values
//          glTexImage2D(GL_TEXTURE_RECTANGLE_NV, 0, textureIntFormat(), mWidth,
//                                  mHeight, 0, textureFormat(), GL_UNSIGNED_BYTE, NULL);
//          // check for errors
//          GET_GLERROR(err);
//
//          // bind the frame buffer
//          glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, mFrameBuffer);
//          
//          // associate the texture with the frame buffer (necessary?)
//          glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, 
//                                                          GL_TEXTURE_RECTANGLE_NV, mTexture, 0);
//          // check for errors
//          GET_GLERROR(err);
//
//          CheckFramebufferStatus();
//  
//          // unbind
//          glBindTexture(GL_TEXTURE_RECTANGLE_NV, 0);
//          glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
//      }
    }
}
//-------------------------------------------------------------
bool
RenSurface::lock()
{
    return ( 0 == pthread_mutex_lock( &mLock ) );
}
//-------------------------------------------------------------
void
RenSurface::unlock()
{
    pthread_mutex_unlock( &mLock );
}
//-------------------------------------------------------------
void
RenSurface::initPBO()
{
    if ( !mPBOAllocated )
    {
        GLenum err;
        // generate a buffer handle
        glGenBuffersARB( 1, &mPixelBuffer );
    
        // check for errors
        GET_GLERROR( err );
        if ( err == GL_NO_ERROR )
        {
            // no error, everything's good to go
            mPBOAllocated = true;
        }
    }
}
//-------------------------------------------------------------
void
RenSurface::cleanupPBO()
{
    if ( mPBOAllocated )
    {
        glDeleteBuffersARB( 1, &mPixelBuffer );
        mPixelBuffer = 0;
        mPBOAllocated = false;
    }
}
//-------------------------------------------------------------
void
RenSurface::initFBO()
{
    if ( !mFBOAllocated )
    {
        GLenum err;
        
        // create the framebuffer and texture handles
        glGenFramebuffersEXT( 1, &mFrameBuffer );
        glGenTextures( 1, &mTexture );

        glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, mFrameBuffer );

        // init texture
        glBindTexture( GL_TEXTURE_RECTANGLE_NV, mTexture );
        glTexImage2D( GL_TEXTURE_RECTANGLE_NV, 0, textureIntFormat(), mWidth,
                      mHeight, 0, textureFormat(), GL_UNSIGNED_BYTE, NULL );
        // check for errors
        GET_GLERROR( err );
        // bail if there was an error
        if ( err != GL_NO_ERROR )
        {
            glDeleteTextures( 1, &mTexture );
            glDeleteFramebuffersEXT( 1, &mFrameBuffer );
            mTexture = mFrameBuffer = 0;
            return;
        }

        glTexParameterf( GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
        glTexParameterf( GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
        glTexParameterf( GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
        glTexParameterf( GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
        glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, 
                                   GL_TEXTURE_RECTANGLE_NV, mTexture, 0 );
        // check for errors
        GET_GLERROR( err );
        // bail if there was an error
        if ( err != GL_NO_ERROR )
        {
            glDeleteTextures( 1, &mTexture );
            glDeleteFramebuffersEXT( 1, &mFrameBuffer );
            mTexture = mFrameBuffer = 0;
            return;
        }

        CheckFramebufferStatus();

        // unbind
        glBindTexture( GL_TEXTURE_RECTANGLE_NV, 0 );
        glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, 0 );

        // success!
        mFBOAllocated = true;
        mAttrModified = false; // just in case
    }
}
//-------------------------------------------------------------
void
RenSurface::cleanupFBO()
{
    if ( mFBOAllocated )
    {
        glDeleteTextures( 1, &mTexture );
        glDeleteFramebuffersEXT( 1, &mFrameBuffer );
        mTexture = mFrameBuffer = 0;
        mFBOAllocated = false;
    }
}
//-------------------------------------------------------------
void
RenSurface::attachToFrameBuffer()
{
    // try to allocate if it hasn't been done yet
    if ( !mFBOAllocated )
    {   
        initFBO();
    }

    // we still have to check since the init can fail
    if ( mFBOAllocated )
    {
        // check to see if the texture attributes have changed
        if ( mAttrModified )
        {
            GLenum err;
            
            // bind the texture
            glBindTexture( GL_TEXTURE_RECTANGLE_NV, mTexture );
            // set it to the new values
            glTexImage2D( GL_TEXTURE_RECTANGLE_NV, 0, textureIntFormat(), mWidth,
                          mHeight, 0, textureFormat(), GL_UNSIGNED_BYTE, NULL );
            // check for errors
            GET_GLERROR( err);

            // bind the frame buffer
            glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, mFrameBuffer );
            
            // associate the texture with the frame buffer (necessary?)
            glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, 
                                       GL_TEXTURE_RECTANGLE_NV, mTexture, 0 );
            // check for errors
            GET_GLERROR( err );

            CheckFramebufferStatus();
    
            // unbind
            glBindTexture( GL_TEXTURE_RECTANGLE_NV, 0 );
            glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, 0 );

            // clear the flag
            mAttrModified = false;
        }

        // bind the framebuffer object
        glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, mFrameBuffer );
        // save the surrent viewport
        glPushAttrib( GL_VIEWPORT_BIT );
        // make the viewport the same size as the texture
        glViewport( 0, 0, mWidth, mHeight );
        
        // adjust flags
        mIsGPUResident = true;
    }
}
//-------------------------------------------------------------
void 
RenSurface::detachFromFrameBuffer()
{
    if ( mFBOAllocated )
    {
        glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, 0 );
        // restore the viewport
        glPopAttrib(); 

        // note that downloadFromGPU is not called
    }
}
//-------------------------------------------------------------
int
RenSurface::uploadToGPU()
{
    // try to allocate if it hasn't been done yet
    if ( !mPBOAllocated )
    {
        initPBO();
    }

    if ( mPBOAllocated )
    {
        // bind the pixel buffer
        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_EXT, mPixelBuffer );
        // set it up to write to the GPU
        glBufferDataARB( GL_PIXEL_UNPACK_BUFFER_EXT, getDataSize(), NULL,
                        GL_STREAM_DRAW_ARB );
        // map the memory
        unsigned char *pixelsPBO = (unsigned char *)glMapBufferARB( GL_PIXEL_UNPACK_BUFFER_EXT,
                                                                    GL_WRITE_ONLY );
        // write to it
        memcpy( pixelsPBO, mData, getDataSize() );
        // unmap the memory
        if ( !glUnmapBufferARB( GL_PIXEL_UNPACK_BUFFER_EXT ) )
        {
            // error, possibly fatal
        }
        // bind the texture
        glBindTexture( GL_TEXTURE_RECTANGLE_NV, mTexture );
        // update the texture object (this tells it where to put the pixel buffer)
        if ( mAttrModified )
        {
            // attributes were modified, use glTexImage2D
            glTexImage2D( GL_TEXTURE_RECTANGLE_NV, 0, textureIntFormat(), mWidth,
                         mHeight, 0, textureFormat(), GL_UNSIGNED_BYTE, NULL );
        }
        else
        {
            glTexSubImage2D( GL_TEXTURE_RECTANGLE_NV, 0, 0,0, mWidth, mHeight,
                            textureFormat(), GL_UNSIGNED_BYTE, BUFFER_OFFSET( 0 ) );
        }
        // unbind the texture
        glBindTexture( GL_TEXTURE_RECTANGLE_NV, 0 );
        // unbind the pixel buffer
        glBindBufferARB( GL_PIXEL_UNPACK_BUFFER_EXT, 0 );

        // adjust flags
        mIsGPUResident = true;
        mAttrModified = false;
    }
}
//-------------------------------------------------------------
int
RenSurface::downloadFromGPU()
{
    // try to allocate if it hasn't been done yet
    if ( !mPBOAllocated )
    {
        initPBO();
    }

    // note that we do not need to check whether the texture attributes were
    // modified. modifyAttributes() fails an assert if it is called when data
    // is resident on the GPU. This means we don't have to worry about nasty
    // things such as buffer sizes changing in ways that would cause us to
    // segfault here.
    
    if ( mPBOAllocated && mFBOAllocated )
    {
        // bind the pixel buffer
        glBindBufferARB( GL_PIXEL_PACK_BUFFER_EXT, mPixelBuffer );
        // set it up to read from the GPU
        glBufferDataARB( GL_PIXEL_PACK_BUFFER_EXT, getDataSize(), NULL,
                         GL_STREAM_READ_ARB );
        // bind the texture
        glBindTexture( GL_TEXTURE_RECTANGLE_NV, mTexture );
        // tell it where you want to copy from
        glGetTexImage( GL_TEXTURE_RECTANGLE_NV, 0, textureFormat(), GL_UNSIGNED_BYTE,
                       BUFFER_OFFSET(0) );
        // map the memory
        unsigned char *pixelsPBO = (unsigned char *)glMapBufferARB( GL_PIXEL_PACK_BUFFER_EXT,
                                                                    GL_READ_ONLY );
        // copy the data
        memcpy( mData, pixelsPBO, getDataSize() );
        // unmap the memory
        if ( !glUnmapBufferARB(GL_PIXEL_PACK_BUFFER_EXT) )
        {
            // error, possibly fatal
        }
        // unbind the texture
        glBindTexture( GL_TEXTURE_RECTANGLE_NV, 0 );
        // unbind the pixel buffer
        glBindBufferARB( GL_PIXEL_PACK_BUFFER_EXT, 0 );

        mIsGPUResident = false;
    }
}
//-------------------------------------------------------------
GLint
RenSurface::textureIntFormat() const
{
    // this is actually the number of bytes in each pixel, or a symbolic
    // constant that represents that to the driver
    // if RGBA -> GL_RBGA or GL_RGB8 (4)
    // if RGB -> GL_RGB or GL_RGB8 (3)
    // if YUV -> GL_LUMINANCE_ALPHA (2?)
    // if grey -> GL_LUMINANCE (1)
    GLint ret;

    switch ( mFormat )
    {
        case GE_FMT_GRAY:
            ret = GL_LUMINANCE;
            break;
        case GE_FMT_YUV422:
            ret = GL_LUMINANCE_ALPHA;
            break;
        case GE_FMT_RGB:
            ret = GL_RGB;
            break;
        case GE_FMT_RGBA:
            ret = GL_RGBA;
            break;
        default:
            ret = 0;
            break;
    }

    return ret;
}
//-------------------------------------------------------------
GLenum
RenSurface::textureFormat() const
{
    // if RGBA -> GL_RGBA
    // if RGB -> GL_RGB
    // if YUV -> GL_LUMINANCE_ALPHA
    // if grey -> GL_LUMINANCE
    GLenum ret;

    switch ( mFormat )
    {
        case GE_FMT_GRAY:
            ret = GL_LUMINANCE;
            break;
        case GE_FMT_YUV422:
            ret = GL_LUMINANCE_ALPHA;
            break;
        case GE_FMT_RGB:
            ret = GL_RGB;
            break;
        case GE_FMT_RGBA:
            ret = GL_RGBA;
            break;
        default:
            ret = 0;
            break;
    }

    return ret;
}
//-------------------------------------------------------------
